/*
 * OBJLoader.java 10 f�vr. 2009
 *
 * Sweet Home 3D, Copyright (c) 2009 Emmanuel PUYBARET / eTeks <info@eteks.com>
 *
 * This program is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation; either version 2 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
 */
package com.eteks.sweethome3d.j3d;

import java.awt.image.BufferedImage;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.StreamTokenizer;
import java.io.StringReader;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.util.ArrayList;
import java.util.ConcurrentModificationException;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import javax.imageio.ImageIO;
import javax.media.j3d.Appearance;
import javax.media.j3d.BranchGroup;
import javax.media.j3d.ColoringAttributes;
import javax.media.j3d.GeometryArray;
import javax.media.j3d.IndexedGeometryArray;
import javax.media.j3d.LineStripArray;
import javax.media.j3d.Material;
import javax.media.j3d.Shape3D;
import javax.media.j3d.TexCoordGeneration;
import javax.media.j3d.Texture;
import javax.media.j3d.TransparencyAttributes;
import javax.vecmath.Color3f;
import javax.vecmath.Point3f;
import javax.vecmath.TexCoord2f;
import javax.vecmath.Vector3f;

import com.sun.j3d.loaders.IncorrectFormatException;
import com.sun.j3d.loaders.Loader;
import com.sun.j3d.loaders.LoaderBase;
import com.sun.j3d.loaders.ParsingErrorException;
import com.sun.j3d.loaders.Scene;
import com.sun.j3d.loaders.SceneBase;
import com.sun.j3d.utils.geometry.GeometryInfo;
import com.sun.j3d.utils.geometry.NormalGenerator;
import com.sun.j3d.utils.image.TextureLoader;

/**
 * An OBJ + MTL loader. 
 * It supports the same features as {@link com.sun.j3d.loaders.objectfile.ObjectFile ObjectFile}
 * Java 3D class, expected for texture images format (supports only BMP, WBMP, GIF, JPEG and PNG format).
 * Compared to <code>ObjectFile</code>, this class supports transparency as defined in
 * <a href="http://local.wasp.uwa.edu.au/~pbourke/dataformats/mtl/">MTL file format</a> 
 * specifications, and doesn't oblige to define texture coordinate on all vertices 
 * when only one face needs such coordinates. Material description is stored in 
 * {@link OBJMaterial OBJMaterial} instances to be able to use additional OBJ information
 * in other circumstances.<br>
 * Note: this class is compatible with Java 3D 1.3.
 * @author Emmanuel Puybaret
 */
public class OBJLoader extends LoaderBase implements Loader
{
	/**
	 * Description of the default Java 3D materials at MTL format 
	 * (copied from com.sun.j3d.loaders.objectfile.DefaultMaterials class with inverse d transparency factor)
	 */
	private static final String JAVA_3D_MATERIALS = "newmtl amber\n" + "Ka 0.0531 0.0531 0.0531\n"
			+ "Kd 0.5755 0.2678 0.0000\n" + "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n" + "Ns 60.0000\n" + "\n"
			+ "newmtl amber_trans\n" + "Ka 0.0531 0.0531 0.0531\n" + "Kd 0.5755 0.2678 0.0000\n"
			+ "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n" + "d 0.1600\n" + "Ns 60.0000\n" + "\n" + "newmtl charcoal\n"
			+ "Ka 0.0082 0.0082 0.0082\n" + "Kd 0.0041 0.0041 0.0041\n" + "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n"
			+ "Ns 60.0000\n" + "\n" + "newmtl lavendar\n" + "Ka 0.1281 0.0857 0.2122\n" + "Kd 0.2187 0.0906 0.3469\n"
			+ "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n" + "Ns 60.0000\n" + "\n" + "newmtl navy_blue\n"
			+ "Ka 0.0000 0.0000 0.0490\n" + "Kd 0.0000 0.0000 0.0531\n" + "Ks 0.1878 0.1878 0.1878\n" + "illum 2\n"
			+ "Ns 91.4700\n" + "\n" + "newmtl pale_green\n" + "Ka 0.0444 0.0898 0.0447\n" + "Kd 0.0712 0.3796 0.0490\n"
			+ "Ks 0.1878 0.1878 0.1878\n" + "illum 2\n" + "Ns 91.4700\n" + "\n" + "newmtl pale_pink\n"
			+ "Ka 0.0898 0.0444 0.0444\n" + "Kd 0.6531 0.2053 0.4160\n" + "Ks 0.1878 0.1878 0.1878\n" + "illum 2\n"
			+ "Ns 91.4700\n" + "\n" + "newmtl pale_yellow\n" + "Ka 0.3606 0.3755 0.0935\n" + "Kd 0.6898 0.6211 0.1999\n"
			+ "Ks 0.1878 0.1878 0.1878\n" + "illum 2\n" + "Ns 91.4700\n" + "\n" + "newmtl peach\n"
			+ "Ka 0.3143 0.1187 0.0167\n" + "Kd 0.6367 0.1829 0.0156\n" + "Ks 0.1878 0.1878 0.1878\n" + "illum 2\n"
			+ "Ns 91.4700\n" + "\n" + "newmtl periwinkle\n" + "Ka 0.0000 0.0000 0.1184\n" + "Kd 0.0000 0.0396 0.8286\n"
			+ "Ks 0.1878 0.1878 0.1878\n" + "illum 2\n" + "Ns 91.4700\n" + "\n" + "newmtl redwood\n"
			+ "Ka 0.0204 0.0027 0.0000\n" + "Kd 0.2571 0.0330 0.0000\n" + "Ks 0.1878 0.1878 0.1878\n" + "illum 2\n"
			+ "Ns 91.4700\n" + "\n" + "newmtl smoked_glass\n" + "Ka 0.0000 0.0000 0.0000\n"
			+ "Kd 0.0041 0.0041 0.0041\n" + "Ks 0.1878 0.1878 0.1878\n" + "illum 2\n" + "d 0.0200\n" + "Ns 91.4700\n"
			+ "\n" + "newmtl aqua_filter\n" + "Ka 0.0000 0.0000 0.0000\n" + "Kd 0.3743 0.6694 0.5791\n"
			+ "Ks 0.1878 0.1878 0.1878\n" + "illum 2\n" + "d 0.0200\n" + "Ns 91.4700\n" + "\n" + "newmtl yellow_green\n"
			+ "Ka 0.0000 0.0000 0.0000\n" + "Kd 0.1875 0.4082 0.0017\n" + "Ks 0.1878 0.1878 0.1878\n" + "illum 2\n"
			+ "Ns 91.4700\n" + "\n" + "newmtl bluetint\n" + "Ka 0.1100 0.4238 0.5388\n" + "Kd 0.0468 0.7115 0.9551\n"
			+ "Ks 0.3184 0.3184 0.3184\n" + "illum 9\n" + "d 0.4300\n" + "Ns 60.0000\n" + "sharpness 60.0000\n" + "\n"
			+ "newmtl plasma\n" + "Ka 0.4082 0.0816 0.2129\n" + "Kd 1.0000 0.0776 0.4478\n"
			+ "Ks 0.3000 0.3000 0.3000\n" + "illum 9\n" + "d 0.2500\n" + "Ns 60.0000\n" + "sharpness 60.0000\n" + "\n"
			+ "newmtl emerald\n" + "Ka 0.0470 1.0000 0.0000\n" + "Kd 0.0470 1.0000 0.0000\n"
			+ "Ks 0.2000 0.2000 0.2000\n" + "illum 9\n" + "d 0.2500\n" + "Ns 60.0000\n" + "sharpness 60.0000\n" + "\n"
			+ "newmtl ruby\n" + "Ka 1.0000 0.0000 0.0000\n" + "Kd 1.0000 0.0000 0.0000\n" + "Ks 0.2000 0.2000 0.2000\n"
			+ "illum 9\n" + "d 0.2500\n" + "Ns 60.0000\n" + "sharpness 60.0000\n" + "\n" + "newmtl sapphire\n"
			+ "Ka 0.0235 0.0000 1.0000\n" + "Kd 0.0235 0.0000 1.0000\n" + "Ks 0.2000 0.2000 0.2000\n" + "illum 9\n"
			+ "d 0.2500\n" + "Ns 60.0000\n" + "sharpness 60.0000\n" + "\n" + "newmtl white\n"
			+ "Ka 0.4000 0.4000 0.4000\n" + "Kd 1.0000 1.0000 1.0000\n" + "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n"
			+ "Ns 60.0000\n" + "\n" + "newmtl red\n" + "Ka 0.4449 0.0000 0.0000\n" + "Kd 0.7714 0.0000 0.0000\n"
			+ "Ks 0.8857 0.0000 0.0000\n" + "illum 2\n" + "Ns 136.4300\n" + "\n" + "newmtl blue_pure\n"
			+ "Ka 0.0000 0.0000 0.5000\n" + "Kd 0.0000 0.0000 1.0000\n" + "Ks 0.0000 0.0000 0.5000\n" + "illum 2\n"
			+ "Ns 65.8900\n" + "\n" + "newmtl lime\n" + "Ka 0.0000 0.5000 0.0000\n" + "Kd 0.0000 1.0000 0.0000\n"
			+ "Ks 0.0000 0.5000 0.0000\n" + "illum 2\n" + "Ns 65.8900\n" + "\n" + "newmtl green\n"
			+ "Ka 0.0000 0.2500 0.0000\n" + "Kd 0.0000 0.2500 0.0000\n" + "Ks 0.0000 0.2500 0.0000\n" + "illum 2\n"
			+ "Ns 65.8900\n" + "\n" + "newmtl yellow\n" + "Ka 1.0000 0.6667 0.0000\n" + "Kd 1.0000 0.6667 0.0000\n"
			+ "Ks 1.0000 0.6667 0.0000\n" + "illum 2\n" + "Ns 65.8900\n" + "\n" + "newmtl purple\n"
			+ "Ka 0.5000 0.0000 1.0000\n" + "Kd 0.5000 0.0000 1.0000\n" + "Ks 0.5000 0.0000 1.0000\n" + "illum 2\n"
			+ "Ns 65.8900\n" + "\n" + "newmtl orange\n" + "Ka 1.0000 0.1667 0.0000\n" + "Kd 1.0000 0.1667 0.0000\n"
			+ "Ks 1.0000 0.1667 0.0000\n" + "illum 2\n" + "Ns 65.8900\n" + "\n" + "newmtl grey\n"
			+ "Ka 0.5000 0.5000 0.5000\n" + "Kd 0.1837 0.1837 0.1837\n" + "Ks 0.5000 0.5000 0.5000\n" + "illum 2\n"
			+ "Ns 65.8900\n" + "\n" + "newmtl rubber\n" + "Ka 0.0000 0.0000 0.0000\n" + "Kd 0.0100 0.0100 0.0100\n"
			+ "Ks 0.1000 0.1000 0.1000\n" + "illum 2\n" + "Ns 65.8900\n" + "\n" + "newmtl flaqua\n"
			+ "Ka 0.0000 0.4000 0.4000\n" + "Kd 0.0000 0.5000 0.5000\n" + "illum 1\n" + "\n" + "newmtl flblack\n"
			+ "Ka 0.0000 0.0000 0.0000\n" + "Kd 0.0041 0.0041 0.0041\n" + "illum 1\n" + "\n" + "newmtl flblue_pure\n"
			+ "Ka 0.0000 0.0000 0.5592\n" + "Kd 0.0000 0.0000 0.7102\n" + "illum 1\n" + "\n" + "newmtl flgrey\n"
			+ "Ka 0.2163 0.2163 0.2163\n" + "Kd 0.5000 0.5000 0.5000\n" + "illum 1\n" + "\n" + "newmtl fllime\n"
			+ "Ka 0.0000 0.3673 0.0000\n" + "Kd 0.0000 1.0000 0.0000\n" + "illum 1\n" + "\n" + "newmtl florange\n"
			+ "Ka 0.6857 0.1143 0.0000\n" + "Kd 1.0000 0.1667 0.0000\n" + "illum 1\n" + "\n" + "newmtl flpurple\n"
			+ "Ka 0.2368 0.0000 0.4735\n" + "Kd 0.3755 0.0000 0.7510\n" + "illum 1\n" + "\n" + "newmtl flred\n"
			+ "Ka 0.4000 0.0000 0.0000\n" + "Kd 1.0000 0.0000 0.0000\n" + "illum 1\n" + "\n" + "newmtl flyellow\n"
			+ "Ka 0.7388 0.4925 0.0000\n" + "Kd 1.0000 0.6667 0.0000\n" + "illum 1\n" + "\n" + "newmtl pink\n"
			+ "Ka 0.9469 0.0078 0.2845\n" + "Kd 0.9878 0.1695 0.6702\n" + "Ks 0.7429 0.2972 0.2972\n" + "illum 2\n"
			+ "Ns 106.2000\n" + "\n" + "newmtl flbrown\n" + "Ka 0.0571 0.0066 0.0011\n" + "Kd 0.1102 0.0120 0.0013\n"
			+ "illum 1\n" + "\n" + "newmtl brown\n" + "Ka 0.1020 0.0185 0.0013\n" + "Kd 0.0857 0.0147 0.0000\n"
			+ "Ks 0.1633 0.0240 0.0000\n" + "illum 2\n" + "Ns 65.8900\n" + "\n" + "newmtl glass\n"
			+ "Ka 1.0000 1.0000 1.0000\n" + "Kd 0.4873 0.4919 0.5306\n" + "Ks 0.6406 0.6939 0.9020\n" + "illum 2\n"
			+ "Ns 200.0000\n" + "\n" + "newmtl flesh\n" + "Ka 0.4612 0.3638 0.2993\n" + "Kd 0.5265 0.4127 0.3374\n"
			+ "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n" + "Ns 60.0000\n" + "\n" + "newmtl aqua\n"
			+ "Ka 0.0000 0.4000 0.4000\n" + "Kd 0.0000 0.5000 0.5000\n" + "Ks 0.5673 0.5673 0.5673\n" + "illum 2\n"
			+ "Ns 60.0000\n" + "\n" + "newmtl black\n" + "Ka 0.0000 0.0000 0.0000\n" + "Kd 0.0020 0.0020 0.0020\n"
			+ "Ks 0.5184 0.5184 0.5184\n" + "illum 2\n" + "Ns 157.3600\n" + "\n" + "newmtl silver\n"
			+ "Ka 0.9551 0.9551 0.9551\n" + "Kd 0.6163 0.6163 0.6163\n" + "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n"
			+ "Ns 60.0000\n" + "\n" + "newmtl dkblue_pure\n" + "Ka 0.0000 0.0000 0.0449\n" + "Kd 0.0000 0.0000 0.1347\n"
			+ "Ks 0.0000 0.0000 0.5673\n" + "illum 2\n" + "Ns 65.8900\n" + "\n" + "newmtl fldkblue_pure\n"
			+ "Ka 0.0000 0.0000 0.0449\n" + "Kd 0.0000 0.0000 0.1347\n" + "illum 1\n" + "\n" + "newmtl dkgreen\n"
			+ "Ka 0.0000 0.0122 0.0000\n" + "Kd 0.0058 0.0245 0.0000\n" + "Ks 0.0000 0.0490 0.0000\n" + "illum 2\n"
			+ "Ns 60.0000\n" + "\n" + "newmtl dkgrey\n" + "Ka 0.0490 0.0490 0.0490\n" + "Kd 0.0490 0.0490 0.0490\n"
			+ "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n" + "Ns 60.0000\n" + "\n" + "newmtl ltbrown\n"
			+ "Ka 0.1306 0.0538 0.0250\n" + "Kd 0.2776 0.1143 0.0531\n" + "Ks 0.3000 0.1235 0.0574\n" + "illum 2\n"
			+ "Ns 60.0000\n" + "\n" + "newmtl fldkgreen\n" + "Ka 0.0000 0.0122 0.0000\n" + "Kd 0.0058 0.0245 0.0000\n"
			+ "illum 1\n" + "\n" + "newmtl flltbrown\n" + "Ka 0.1306 0.0538 0.0250\n" + "Kd 0.2776 0.1143 0.0531\n"
			+ "illum 1\n" + "\n" + "newmtl tan\n" + "Ka 0.4000 0.3121 0.1202\n" + "Kd 0.6612 0.5221 0.2186\n"
			+ "Ks 0.5020 0.4118 0.2152\n" + "illum 2\n" + "Ns 60.0000\n" + "\n" + "newmtl fltan\n"
			+ "Ka 0.4000 0.3121 0.1202\n" + "Kd 0.6612 0.4567 0.1295\n" + "illum 1\n" + "\n" + "newmtl brzskin\n"
			+ "Ka 0.4408 0.2694 0.1592\n" + "Kd 0.3796 0.2898 0.2122\n" + "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n"
			+ "Ns 25.0000\n" + "\n" + "newmtl lips\n" + "Ka 0.4408 0.2694 0.1592\n" + "Kd 0.9265 0.2612 0.2898\n"
			+ "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n" + "Ns 25.0000\n" + "\n" + "newmtl redorange\n"
			+ "Ka 0.3918 0.0576 0.0000\n" + "Kd 0.7551 0.0185 0.0000\n" + "Ks 0.4694 0.3224 0.1667\n" + "illum 2\n"
			+ "Ns 132.5600\n" + "\n" + "newmtl blutan\n" + "Ka 0.4408 0.2694 0.1592\n" + "Kd 0.0776 0.2571 0.2041\n"
			+ "Ks 0.1467 0.1469 0.0965\n" + "illum 2\n" + "Ns 25.0000\n" + "\n" + "newmtl bluteal\n"
			+ "Ka 0.0041 0.1123 0.1224\n" + "Kd 0.0776 0.2571 0.2041\n" + "Ks 0.1467 0.1469 0.0965\n" + "illum 2\n"
			+ "Ns 25.0000\n" + "\n" + "newmtl pinktan\n" + "Ka 0.4408 0.2694 0.1592\n" + "Kd 0.6857 0.2571 0.2163\n"
			+ "Ks 0.1467 0.1469 0.0965\n" + "illum 2\n" + "Ns 25.0000\n" + "\n" + "newmtl brnhair\n"
			+ "Ka 0.0612 0.0174 0.0066\n" + "Kd 0.0898 0.0302 0.0110\n" + "Ks 0.1306 0.0819 0.0352\n" + "illum 2\n"
			+ "Ns 60.4700\n" + "\n" + "newmtl blondhair\n" + "Ka 0.4449 0.2632 0.0509\n" + "Kd 0.5714 0.3283 0.0443\n"
			+ "Ks 0.7755 0.4602 0.0918\n" + "illum 2\n" + "Ns 4.6500\n" + "\n" + "newmtl flblonde\n"
			+ "Ka 0.4449 0.2632 0.0509\n" + "Kd 0.5714 0.3283 0.0443\n" + "illum 1\n" + "\n" + "newmtl yelloworng\n"
			+ "Ka 0.5837 0.1715 0.0000\n" + "Kd 0.8857 0.2490 0.0000\n" + "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n"
			+ "Ns 60.0000\n" + "\n" + "newmtl bone\n" + "Ka 0.3061 0.1654 0.0650\n" + "Kd 0.9000 0.7626 0.4261\n"
			+ "Ks 0.8939 0.7609 0.5509\n" + "illum 2\n" + "Ns 200.0000\n" + "\n" + "newmtl teeth\n"
			+ "Ka 0.6408 0.5554 0.3845\n" + "Kd 0.9837 0.7959 0.4694\n" + "illum 1\n" + "\n" + "newmtl brass\n"
			+ "Ka 0.2490 0.1102 0.0000\n" + "Kd 0.4776 0.1959 0.0000\n" + "Ks 0.5796 0.5796 0.5796\n" + "illum 2\n"
			+ "Ns 134.8800\n" + "\n" + "newmtl dkred\n" + "Ka 0.0939 0.0000 0.0000\n" + "Kd 0.2286 0.0000 0.0000\n"
			+ "Ks 0.2490 0.0000 0.0000\n" + "illum 2\n" + "Ns 60.0000\n" + "\n" + "newmtl taupe\n"
			+ "Ka 0.1061 0.0709 0.0637\n" + "Kd 0.2041 0.1227 0.1058\n" + "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n"
			+ "Ns 84.5000\n" + "\n" + "newmtl dkteal\n" + "Ka 0.0000 0.0245 0.0163\n" + "Kd 0.0000 0.0653 0.0449\n"
			+ "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n" + "Ns 55.0400\n" + "\n" + "newmtl dkdkgrey\n"
			+ "Ka 0.0000 0.0000 0.0000\n" + "Kd 0.0122 0.0122 0.0122\n" + "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n"
			+ "Ns 60.0000\n" + "\n" + "newmtl dkblue\n" + "Ka 0.0000 0.0029 0.0408\n" + "Kd 0.0000 0.0041 0.0571\n"
			+ "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n" + "Ns 60.0000\n" + "\n" + "newmtl gold\n"
			+ "Ka 0.7224 0.1416 0.0000\n" + "Kd 1.0000 0.4898 0.0000\n" + "Ks 0.7184 0.3695 0.3695\n" + "illum 2\n"
			+ "Ns 123.2600\n" + "\n" + "newmtl redbrick\n" + "Ka 0.1102 0.0067 0.0067\n" + "Kd 0.3306 0.0398 0.0081\n"
			+ "illum 1\n" + "\n" + "newmtl flmustard\n" + "Ka 0.4245 0.2508 0.0000\n" + "Kd 0.8898 0.3531 0.0073\n"
			+ "illum 1\n" + "\n" + "newmtl flpinegreen\n" + "Ka 0.0367 0.0612 0.0204\n" + "Kd 0.1061 0.2163 0.0857\n"
			+ "illum 1\n" + "\n" + "newmtl fldkred\n" + "Ka 0.0939 0.0000 0.0000\n" + "Kd 0.2286 0.0082 0.0082\n"
			+ "illum 1\n" + "\n" + "newmtl fldkgreen2\n" + "Ka 0.0025 0.0122 0.0014\n" + "Kd 0.0245 0.0694 0.0041\n"
			+ "illum 1\n" + "\n" + "newmtl flmintgreen\n" + "Ka 0.0408 0.1429 0.0571\n" + "Kd 0.1306 0.2898 0.1673\n"
			+ "illum 1\n" + "\n" + "newmtl olivegreen\n" + "Ka 0.0167 0.0245 0.0000\n" + "Kd 0.0250 0.0367 0.0000\n"
			+ "Ks 0.2257 0.2776 0.1167\n" + "illum 2\n" + "Ns 97.6700\n" + "\n" + "newmtl skin\n"
			+ "Ka 0.2286 0.0187 0.0187\n" + "Kd 0.1102 0.0328 0.0139\n" + "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n"
			+ "Ns 17.8300\n" + "\n" + "newmtl redbrown\n" + "Ka 0.1469 0.0031 0.0000\n" + "Kd 0.2816 0.0060 0.0000\n"
			+ "Ks 0.3714 0.3714 0.3714\n" + "illum 2\n" + "Ns 141.0900\n" + "\n" + "newmtl deepgreen\n"
			+ "Ka 0.0000 0.0050 0.0000\n" + "Kd 0.0000 0.0204 0.0050\n" + "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n"
			+ "Ns 113.1800\n" + "\n" + "newmtl flltolivegreen\n" + "Ka 0.0167 0.0245 0.0000\n"
			+ "Kd 0.0393 0.0531 0.0100\n" + "illum 1\n" + "\n" + "newmtl jetflame\n" + "Ka 0.7714 0.0000 0.0000\n"
			+ "Kd 0.9510 0.4939 0.0980\n" + "Ks 0.8531 0.5222 0.0000\n" + "illum 2\n" + "Ns 132.5600\n" + "\n"
			+ "newmtl brownskn\n" + "Ka 0.0122 0.0041 0.0000\n" + "Kd 0.0204 0.0082 0.0000\n"
			+ "Ks 0.0735 0.0508 0.0321\n" + "illum 2\n" + "Ns 20.1600\n" + "\n" + "newmtl greenskn\n"
			+ "Ka 0.0816 0.0449 0.0000\n" + "Kd 0.0000 0.0735 0.0000\n" + "Ks 0.0490 0.1224 0.0898\n" + "illum 3\n"
			+ "Ns 46.5100\n" + "sharpness 146.5100\n" + "\n" + "newmtl ltgrey\n" + "Ka 0.5000 0.5000 0.5000\n"
			+ "Kd 0.3837 0.3837 0.3837\n" + "Ks 0.5000 0.5000 0.5000\n" + "illum 2\n" + "Ns 65.8900\n" + "\n"
			+ "newmtl bronze\n" + "Ka 0.0449 0.0204 0.0000\n" + "Kd 0.0653 0.0367 0.0122\n"
			+ "Ks 0.0776 0.0408 0.0000\n" + "illum 3\n" + "Ns 137.2100\n" + "sharpness 125.5800\n" + "\n"
			+ "newmtl bone1\n" + "Ka 0.6408 0.5554 0.3845\n" + "Kd 0.9837 0.7959 0.4694\n" + "illum 1\n" + "\n"
			+ "newmtl flwhite1\n" + "Ka 0.9306 0.9306 0.9306\n" + "Kd 1.0000 1.0000 1.0000\n" + "illum 1\n" + "\n"
			+ "newmtl flwhite\n" + "Ka 0.6449 0.6116 0.5447\n" + "Kd 0.9837 0.9309 0.8392\n"
			+ "Ks 0.8082 0.7290 0.5708\n" + "illum 2\n" + "Ns 200.0000\n" + "\n" + "newmtl shadow\n"
			+ "Kd 0.0350 0.0248 0.0194\n" + "illum 0\n" + "d 0.2500\n" + "\n" + "newmtl fldkolivegreen\n"
			+ "Ka 0.0056 0.0082 0.0000\n" + "Kd 0.0151 0.0204 0.0038\n" + "illum 1\n" + "\n" + "newmtl fldkdkgrey\n"
			+ "Ka 0.0000 0.0000 0.0000\n" + "Kd 0.0122 0.0122 0.0122\n" + "illum 1\n" + "\n" + "newmtl lcdgreen\n"
			+ "Ka 0.4000 0.4000 0.4000\n" + "Kd 0.5878 1.0000 0.5061\n" + "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n"
			+ "Ns 60.0000\n" + "\n" + "newmtl brownlips\n" + "Ka 0.1143 0.0694 0.0245\n" + "Kd 0.1429 0.0653 0.0408\n"
			+ "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n" + "Ns 25.0000\n" + "\n" + "newmtl muscle\n"
			+ "Ka 0.2122 0.0077 0.0154\n" + "Kd 0.4204 0.0721 0.0856\n" + "Ks 0.1184 0.1184 0.1184\n" + "illum 2\n"
			+ "Ns 25.5800\n" + "\n" + "newmtl flltgrey\n" + "Ka 0.5224 0.5224 0.5224\n" + "Kd 0.8245 0.8245 0.8245\n"
			+ "illum 1\n" + "\n" + "newmtl offwhite.warm\n" + "Ka 0.5184 0.4501 0.3703\n" + "Kd 0.8367 0.6898 0.4490\n"
			+ "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n" + "Ns 60.0000\n" + "\n" + "newmtl offwhite.cool\n"
			+ "Ka 0.5184 0.4501 0.3703\n" + "Kd 0.8367 0.6812 0.5703\n" + "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n"
			+ "Ns 60.0000\n" + "\n" + "newmtl yellowbrt\n" + "Ka 0.4000 0.4000 0.4000\n" + "Kd 1.0000 0.7837 0.0000\n"
			+ "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n" + "Ns 60.0000\n" + "\n" + "newmtl chappie\n"
			+ "Ka 0.4000 0.4000 0.4000\n" + "Kd 0.5837 0.1796 0.0367\n" + "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n"
			+ "Ns 60.0000\n" + "\n" + "newmtl archwhite\n" + "Ka 0.2816 0.2816 0.2816\n" + "Kd 0.9959 0.9959 0.9959\n"
			+ "illum 1\n" + "\n" + "newmtl archwhite2\n" + "Ka 0.2816 0.2816 0.2816\n" + "Kd 0.8408 0.8408 0.8408\n"
			+ "illum 1\n" + "\n" + "newmtl lighttan\n" + "Ka 0.0980 0.0536 0.0220\n" + "Kd 0.7020 0.4210 0.2206\n"
			+ "Ks 0.8286 0.8057 0.5851\n" + "illum 2\n" + "Ns 177.5200\n" + "\n" + "newmtl lighttan2\n"
			+ "Ka 0.0980 0.0492 0.0144\n" + "Kd 0.3143 0.1870 0.0962\n" + "Ks 0.8286 0.8057 0.5851\n" + "illum 2\n"
			+ "Ns 177.5200\n" + "\n" + "newmtl lighttan3\n" + "Ka 0.0980 0.0492 0.0144\n" + "Kd 0.1796 0.0829 0.0139\n"
			+ "Ks 0.8286 0.8057 0.5851\n" + "illum 2\n" + "Ns 177.5200\n" + "\n" + "newmtl lightyellow\n"
			+ "Ka 0.5061 0.1983 0.0000\n" + "Kd 1.0000 0.9542 0.3388\n" + "Ks 1.0000 0.9060 0.0000\n" + "illum 2\n"
			+ "Ns 177.5200\n" + "\n" + "newmtl lighttannew\n" + "Ka 0.0980 0.0492 0.0144\n"
			+ "Kd 0.7878 0.6070 0.3216\n" + "Ks 0.8286 0.8057 0.5851\n" + "illum 2\n" + "Ns 177.5200\n" + "\n"
			+ "newmtl default\n" + "Ka 0.4000 0.4000 0.4000\n" + "Kd 0.7102 0.7020 0.6531\n"
			+ "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n" + "Ns 128.0000\n" + "\n" + "newmtl ship2\n"
			+ "Ka 0.0000 0.0000 0.0000\n" + "Kd 1.0000 1.0000 1.0000\n" + "Ks 0.1143 0.1143 0.1143\n" + "illum 2\n"
			+ "Ns 60.0000\n" + "\n" + "newmtl dkpurple\n" + "Ka 0.0082 0.0000 0.0163\n" + "Kd 0.0245 0.0000 0.0490\n"
			+ "Ks 0.1266 0.0000 0.2531\n" + "illum 2\n" + "Ns 65.8900\n" + "\n" + "newmtl dkorange\n"
			+ "Ka 0.4041 0.0123 0.0000\n" + "Kd 0.7143 0.0350 0.0000\n" + "Ks 0.7102 0.0870 0.0000\n" + "illum 2\n"
			+ "Ns 65.8900\n" + "\n" + "newmtl mintgrn\n" + "Ka 0.0101 0.1959 0.0335\n" + "Kd 0.0245 0.4776 0.0816\n"
			+ "Ks 0.0245 0.4776 0.0816\n" + "illum 2\n" + "Ns 65.8900\n" + "\n" + "newmtl fgreen\n"
			+ "Ka 0.0000 0.0449 0.0000\n" + "Kd 0.0000 0.0449 0.0004\n" + "Ks 0.0062 0.0694 0.0000\n" + "illum 2\n"
			+ "Ns 106.2000\n" + "\n" + "newmtl glassblutint\n" + "Ka 0.4000 0.4000 0.4000\n"
			+ "Kd 0.5551 0.8000 0.7730\n" + "Ks 0.7969 0.9714 0.9223\n" + "illum 4\n" + "d 0.6700\n" + "Ns 60.0000\n"
			+ "sharpness 60.0000\n" + "\n" + "newmtl bflesh\n" + "Ka 0.0122 0.0122 0.0122\n"
			+ "Kd 0.0245 0.0081 0.0021\n" + "Ks 0.0531 0.0460 0.0153\n" + "illum 2\n" + "Ns 20.1600\n" + "\n"
			+ "newmtl meh\n" + "Ka 0.4000 0.4000 0.4000\n" + "Kd 0.5551 0.8000 0.7730\n" + "Ks 0.7969 0.9714 0.9223\n"
			+ "illum 4\n" + "d 0.2500\n" + "Ns 183.7200\n" + "sharpness 60.0000\n" + "\n" + "newmtl violet\n"
			+ "Ka 0.0083 0.0000 0.1265\n" + "Kd 0.0287 0.0269 0.1347\n" + "Ks 0.2267 0.4537 0.6612\n" + "illum 2\n"
			+ "Ns 96.9000\n" + "\n" + "newmtl iris\n" + "Ka 0.3061 0.0556 0.0037\n" + "Kd 0.0000 0.0572 0.3184\n"
			+ "Ks 0.8041 0.6782 0.1477\n" + "illum 2\n" + "Ns 188.3700\n" + "\n" + "newmtl blugrn\n"
			+ "Ka 0.4408 0.4144 0.1592\n" + "Kd 0.0811 0.6408 0.2775\n" + "Ks 0.1467 0.1469 0.0965\n" + "illum 2\n"
			+ "Ns 25.0000\n" + "\n" + "newmtl glasstransparent\n" + "Ka 0.2163 0.2163 0.2163\n"
			+ "Kd 0.4694 0.4694 0.4694\n" + "Ks 0.6082 0.6082 0.6082\n" + "illum 4\n" + "d 0.2500\n" + "Ns 200.0000\n"
			+ "sharpness 60.0000\n" + "\n" + "newmtl fleshtransparent\n" + "Ka 0.4000 0.2253 0.2253\n"
			+ "Kd 0.6898 0.2942 0.1295\n" + "Ks 0.7388 0.4614 0.4614\n" + "illum 4\n" + "d 0.2500\n" + "Ns 6.2000\n"
			+ "sharpness 60.0000\n" + "\n" + "newmtl fldkgrey\n" + "Ka 0.0449 0.0449 0.0449\n"
			+ "Kd 0.0939 0.0939 0.0939\n" + "illum 1\n" + "\n" + "newmtl sky_blue\n" + "Ka 0.1363 0.2264 0.4122\n"
			+ "Kd 0.1241 0.5931 0.8000\n" + "Ks 0.0490 0.0490 0.0490\n" + "illum 2\n" + "Ns 13.9500\n" + "\n"
			+ "newmtl fldkpurple\n" + "Ka 0.0443 0.0257 0.0776\n" + "Kd 0.1612 0.0000 0.3347\n"
			+ "Ks 0.0000 0.0000 0.0000\n" + "illum 2\n" + "Ns 13.9500\n" + "\n" + "newmtl dkbrown\n"
			+ "Ka 0.0143 0.0062 0.0027\n" + "Kd 0.0087 0.0038 0.0016\n" + "Ks 0.2370 0.2147 0.1821\n" + "illum 3\n"
			+ "Ns 60.0000\n" + "sharpness 60.0000\n" + "\n" + "newmtl bone2\n" + "Ka 0.6408 0.5388 0.3348\n"
			+ "Kd 0.9837 0.8620 0.6504\n" + "illum 1\n" + "\n" + "newmtl bluegrey\n" + "Ka 0.4000 0.4000 0.4000\n"
			+ "Kd 0.1881 0.2786 0.2898\n" + "Ks 0.3000 0.3000 0.3000\n" + "illum 2\n" + "Ns 14.7300\n" + "\n"
			+ "newmtl metal\n" + "Ka 0.9102 0.8956 0.1932\n" + "Kd 0.9000 0.7626 0.4261\n" + "Ks 0.8939 0.8840 0.8683\n"
			+ "illum 2\n" + "Ns 200.0000\n" + "\n" + "newmtl sand_stone\n" + "Ka 0.1299 0.1177 0.0998\n"
			+ "Kd 0.1256 0.1138 0.0965\n" + "Ks 0.2370 0.2147 0.1821\n" + "illum 3\n" + "Ns 60.0000\n"
			+ "sharpness 60.0000\n" + "\n" + "newmtl hair\n" + "Ka 0.0013 0.0012 0.0010\n" + "Kd 0.0008 0.0007 0.0006\n"
			+ "Ks 0.0000 0.0000 0.0000\n" + "illum 3\n" + "Ns 60.0000\n" + "sharpness 60.0000\n";
	private final static Map<String, Appearance> DEFAULT_APPEARANCES;
	
	static
	{
		try
		{
			DEFAULT_APPEARANCES = parseMaterialStream(new StringReader(JAVA_3D_MATERIALS), null, null);
		}
		catch (IOException ex)
		{
			// Can't happen because materials are read from a string
			throw new InternalError("Can't access to default materials");
		}
	}
	
	private Boolean useCaches;
	private List<Point3f> vertices;
	private List<TexCoord2f> textureCoordinates;
	private List<Vector3f> normals;
	private Map<String, Group> groups;
	private Group currentGroup;
	private String currentMaterial;
	private Map<String, Appearance> appearances;
	
	/**
	 * Sets whether this loader should try or avoid accessing to URLs with cache.
	 * @param useCaches <code>Boolean.TRUE</code>, <code>Boolean.FALSE</code>, or 
	 *    <code>null</code> then caches will be used according to the value 
	 *    returned by {@link URLConnection#getDefaultUseCaches()}.
	 */
	public void setUseCaches(Boolean useCaches)
	{
		this.useCaches = Boolean.valueOf(useCaches);
	}
	
	/**
	 * Returns the scene described in the given OBJ file.
	 */
	public Scene load(String file) throws FileNotFoundException, IncorrectFormatException, ParsingErrorException
	{
		URL baseUrl;
		try
		{
			if (this.basePath != null)
			{
				baseUrl = new File(this.basePath).toURI().toURL();
			}
			else
			{
				baseUrl = new File(file).toURI().toURL();
			}
		}
		catch (MalformedURLException ex)
		{
			throw new FileNotFoundException(file);
		}
		try
		{
			return load(new InputStreamReader(new FileInputStream(file), "ISO-8859-1"), baseUrl);
		}
		catch (UnsupportedEncodingException ex)
		{
			// Shouldn't happen 
			return load(new InputStreamReader(new FileInputStream(file)));
		}
	}
	
	/**
	 * Returns the scene described in the given OBJ file URL.
	 */
	public Scene load(URL url) throws FileNotFoundException, IncorrectFormatException, ParsingErrorException
	{
		URL baseUrl = this.baseUrl;
		if (this.baseUrl == null)
		{
			baseUrl = url;
		}
		InputStream in;
		try
		{
			in = openStream(url, this.useCaches);
		}
		catch (IOException ex)
		{
			throw new FileNotFoundException("Can't read " + url);
		}
		try
		{
			return load(new InputStreamReader(in, "ISO-8859-1"), baseUrl);
		}
		catch (UnsupportedEncodingException ex)
		{
			// Shouldn't happen 
			return load(new InputStreamReader(in));
		}
	}
	
	/**
	 * Returns an input stream ready to read data from the given URL.
	 */
	private static InputStream openStream(URL url, Boolean useCaches) throws IOException
	{
		URLConnection connection = url.openConnection();
		if (useCaches != null)
		{
			connection.setUseCaches(useCaches.booleanValue());
		}
		return connection.getInputStream();
	}
	
	/**
	 * Returns the scene described in the given OBJ file stream.
	 */
	public Scene load(Reader reader) throws FileNotFoundException, IncorrectFormatException, ParsingErrorException
	{
		return load(reader, null);
	}
	
	/**
	 * Returns the scene described in the given OBJ file.
	 */
	private Scene load(Reader reader, URL baseUrl) throws FileNotFoundException
	{
		if (!(reader instanceof BufferedReader))
		{
			reader = new BufferedReader(reader);
		}
		try
		{
			return parseObjectStream(reader, baseUrl);
		}
		catch (IOException ex)
		{
			throw new ParsingErrorException(ex.getMessage());
		}
		finally
		{
			try
			{
				reader.close();
			}
			catch (IOException ex)
			{
				throw new ParsingErrorException(ex.getMessage());
			}
		}
	}
	
	/**
	 * Returns the scene parsed from a stream. 
	 */
	private Scene parseObjectStream(Reader reader, URL baseUrl) throws IOException
	{
		this.vertices = new ArrayList<Point3f>();
		this.textureCoordinates = new ArrayList<TexCoord2f>();
		this.normals = new ArrayList<Vector3f>();
		this.groups = new LinkedHashMap<String, Group>();
		this.currentGroup = new Group("default");
		this.groups.put("default", this.currentGroup);
		this.currentMaterial = "default";
		this.appearances = new HashMap<String, Appearance>(DEFAULT_APPEARANCES);
		
		StreamTokenizer tokenizer = createTokenizer(reader);
		while (tokenizer.nextToken() != StreamTokenizer.TT_EOF)
		{
			switch (tokenizer.ttype)
			{
				case StreamTokenizer.TT_WORD:
					parseObjectLine(tokenizer, baseUrl);
					break;
				case StreamTokenizer.TT_EOL:
					break;
				default:
					throw new IncorrectFormatException(
							"Unexpected token " + tokenizer.sval + " at row " + tokenizer.lineno());
			}
		}
		
		try
		{
			return createScene();
		}
		finally
		{
			this.vertices = null;
			this.textureCoordinates = null;
			this.normals = null;
			this.groups = null;
			this.appearances = null;
		}
	}
	
	/**
	 * Returns a new scene created from the parsed objects. 
	 */
	private SceneBase createScene()
	{
		Point3f[] vertices = this.vertices.toArray(new Point3f[this.vertices.size()]);
		TexCoord2f[] textureCoordinates = this.textureCoordinates
				.toArray(new TexCoord2f[this.textureCoordinates.size()]);
		Vector3f[] normals = this.normals.toArray(new Vector3f[this.normals.size()]);
		
		SceneBase scene = new SceneBase();
		BranchGroup sceneRoot = new BranchGroup();
		scene.setSceneGroup(sceneRoot);
		for (Group group : this.groups.values())
		{
			List<Geometry> geometries = group.getGeometries();
			if (geometries != null && !geometries.isEmpty())
			{
				int i = 0;
				while (i < geometries.size())
				{
					Geometry firstGeometry = geometries.get(i);
					boolean firstGeometryHasTextureCoordinateIndices = firstGeometry.hasTextureCoordinateIndices();
					boolean firstFaceHasNormalIndices = (firstGeometry instanceof Face)
							&& ((Face) firstGeometry).hasNormalIndices();
					
					String firstGeometryMaterial = firstGeometry.getMaterial();
					Appearance appearance = getAppearance(firstGeometryMaterial);
					
					// Search how many geometries share the same characteristics 
					int max = i;
					while (++max < geometries.size())
					{
						Geometry geometry = geometries.get(max);
						String material = geometry.getMaterial();
						if (geometry.getClass() != firstGeometry.getClass()
								|| material == null && firstGeometryMaterial != null
								|| material != null && getAppearance(material) != appearance
								|| (firstGeometryHasTextureCoordinateIndices ^ geometry.hasTextureCoordinateIndices())
								|| (firstFaceHasNormalIndices
										^ ((geometry instanceof Face) && ((Face) geometry).hasNormalIndices())))
						{
							break;
						}
					}
					
					// Create indices arrays for the geometries with an index between i and max
					int geometryCount = max - i;
					int indexCount = 0;
					for (int j = 0; j < geometryCount; j++)
					{
						indexCount += geometries.get(i + j).getVertexIndices().length;
					}
					int[] coordinatesIndices = new int[indexCount];
					int[] stripCounts = new int[geometryCount];
					for (int j = 0, destIndex = 0; j < geometryCount; j++)
					{
						int[] geometryVertexIndices = geometries.get(i + j).getVertexIndices();
						System.arraycopy(geometryVertexIndices, 0, coordinatesIndices, destIndex,
								geometryVertexIndices.length);
						stripCounts[j] = geometryVertexIndices.length;
						destIndex += geometryVertexIndices.length;
					}
					
					int[] textureCoordinateIndices = null;
					if (firstGeometryHasTextureCoordinateIndices)
					{
						textureCoordinateIndices = new int[indexCount];
						for (int j = 0, destIndex = 0; j < geometryCount; j++)
						{
							int[] geometryTextureCoordinateIndices = geometries.get(i + j)
									.getTextureCoordinateIndices();
							System.arraycopy(geometryTextureCoordinateIndices, 0, textureCoordinateIndices, destIndex,
									geometryTextureCoordinateIndices.length);
							destIndex += geometryTextureCoordinateIndices.length;
						}
					}
					
					GeometryArray geometryArray;
					if (firstGeometry instanceof Face)
					{
						GeometryInfo geometryInfo = new GeometryInfo(GeometryInfo.POLYGON_ARRAY);
						geometryInfo.setCoordinates(vertices);
						geometryInfo.setCoordinateIndices(coordinatesIndices);
						geometryInfo.setStripCounts(stripCounts);
						
						if (firstGeometryHasTextureCoordinateIndices)
						{
							geometryInfo.setTextureCoordinateParams(1, 2);
							geometryInfo.setTextureCoordinates(0, textureCoordinates);
							geometryInfo.setTextureCoordinateIndices(0, textureCoordinateIndices);
						}
						
						if (firstFaceHasNormalIndices)
						{
							int[] normalIndices = new int[indexCount];
							for (int j = 0, destIndex = 0; j < geometryCount; j++)
							{
								int[] faceNormalIndices = ((Face) geometries.get(i + j)).getNormalIndices();
								System.arraycopy(faceNormalIndices, 0, normalIndices, destIndex,
										faceNormalIndices.length);
								destIndex += faceNormalIndices.length;
							}
							geometryInfo.setNormals(normals);
							geometryInfo.setNormalIndices(normalIndices);
						}
						else
						{
							NormalGenerator normalGenerator = new NormalGenerator(Math.PI / 2);
							if (!group.isSmooth())
							{
								normalGenerator.setCreaseAngle(0);
							}
							normalGenerator.generateNormals(geometryInfo);
						}
						geometryArray = geometryInfo.getGeometryArray(true, true, false);
					}
					else
					{ // Line
						int format = IndexedGeometryArray.COORDINATES;
						if (firstGeometryHasTextureCoordinateIndices)
						{
							format |= IndexedGeometryArray.TEXTURE_COORDINATE_2;
						}
						
						// Use non indexed line array to avoid referencing the whole vertices
						geometryArray = new LineStripArray(coordinatesIndices.length, format, stripCounts);
						for (int j = 0; j < coordinatesIndices.length; j++)
						{
							geometryArray.setCoordinate(j, vertices[coordinatesIndices[j]]);
						}
						if (firstGeometryHasTextureCoordinateIndices)
						{
							for (int j = 0; j < coordinatesIndices.length; j++)
							{
								geometryArray.setTextureCoordinate(0, j,
										textureCoordinates[textureCoordinateIndices[j]]);
							}
						}
					}
					
					// Clone appearance to avoid sharing it
					if (appearance != null)
					{
						appearance = (Appearance) appearance.cloneNodeComponent(false);
						// Create texture coordinates if geometry doesn't define its own coordinates 
						// and appearance contains a texture 
						if (!firstGeometryHasTextureCoordinateIndices && appearance.getTexture() != null)
						{
							appearance.setTexCoordGeneration(new TexCoordGeneration());
						}
					}
					Shape3D shape = new Shape3D(geometryArray, appearance);
					sceneRoot.addChild(shape);
					scene.addNamedObject(group.getName() + (i == 0 ? "" : String.valueOf(i)), shape);
					
					i = max;
				}
			}
		}
		return scene;
	}
	
	/**
	 * Returns the appearance matching a given <code>material</code>. 
	 */
	private Appearance getAppearance(String material)
	{
		Appearance appearance = null;
		if (material != null)
		{
			appearance = this.appearances.get(material);
		}
		if (appearance == null)
		{
			appearance = DEFAULT_APPEARANCES.get("default");
		}
		return appearance;
	}
	
	/**
	 * Parses the line starting with a word.
	 */
	private void parseObjectLine(StreamTokenizer tokenizer, URL baseUrl) throws IOException
	{
		if ("v".equals(tokenizer.sval))
		{
			// Read vertex v x y z
			float x = parseNumber(tokenizer);
			skipBackSlash(tokenizer);
			float y = parseNumber(tokenizer);
			skipBackSlash(tokenizer);
			float z = parseNumber(tokenizer);
			this.vertices.add(new Point3f(x, y, z));
			// Skip next number if it exists
			if (tokenizer.nextToken() == StreamTokenizer.TT_EOL)
			{
				tokenizer.pushBack();
			}
		}
		else if ("vn".equals(tokenizer.sval))
		{
			// Read normal vn x y z
			float x = parseNumber(tokenizer);
			skipBackSlash(tokenizer);
			float y = parseNumber(tokenizer);
			skipBackSlash(tokenizer);
			float z = parseNumber(tokenizer);
			this.normals.add(new Vector3f(x, y, z));
		}
		else if ("vt".equals(tokenizer.sval))
		{
			// Read texture coordinate vt x y 
			//                       or vt x y z
			float x = parseNumber(tokenizer);
			skipBackSlash(tokenizer);
			float y = parseNumber(tokenizer);
			this.textureCoordinates.add(new TexCoord2f(x, y));
			// Skip next number if it exists
			if (tokenizer.nextToken() == StreamTokenizer.TT_EOL)
			{
				tokenizer.pushBack();
			}
		}
		else if ("l".equals(tokenizer.sval))
		{
			tokenizer.ordinaryChar('/');
			// Read line l v       v       v       ...
			//        or l v/vt    v/vt    v/vt    ...
			List<Integer> vertexIndices = new ArrayList<Integer>(2);
			List<Integer> textureCoordinateIndices = new ArrayList<Integer>(2);
			boolean first = true;
			while (true)
			{
				if (first)
				{
					first = false;
				}
				else
				{
					skipBackSlash(tokenizer);
				}
				if (tokenizer.nextToken() == StreamTokenizer.TT_EOL)
				{
					break;
				}
				else
				{
					tokenizer.pushBack();
				}
				// Read vertex index
				int vertexIndex = parseInteger(tokenizer) - 1;
				if (vertexIndex < 0)
				{
					vertexIndex += this.vertices.size() + 1;
				}
				vertexIndices.add(vertexIndex);
				
				if (tokenizer.nextToken() != '/')
				{
					// l v  
					tokenizer.pushBack();
				}
				else
				{
					// l v/vt : read texture coordinate index 
					int textureCoordinateIndex = parseInteger(tokenizer) - 1;
					if (textureCoordinateIndex < 0)
					{
						textureCoordinateIndex += this.textureCoordinates.size() + 1;
					}
					textureCoordinateIndices.add(textureCoordinateIndex);
				}
			}
			tokenizer.pushBack();
			tokenizer.wordChars('/', '/');
			if (textureCoordinateIndices.size() != 0 && textureCoordinateIndices.size() != vertexIndices.size())
			{
				// Ignore unconsistent texture coordinate 
				textureCoordinateIndices.clear();
			}
			if (vertexIndices.size() > 1)
			{
				this.currentGroup.addGeometry(new Line(vertexIndices, textureCoordinateIndices, this.currentMaterial));
			}
		}
		else if ("f".equals(tokenizer.sval))
		{
			tokenizer.ordinaryChar('/');
			// Read face f v       v       v       ...
			//        or f v//vn   v//vn   v//vn   ...
			//        or f v/vt    v/vt    v/vt    ...
			//        or f v/vt/vn v/vt/vn v/vt/vn ...
			List<Integer> vertexIndices = new ArrayList<Integer>(4);
			List<Integer> textureCoordinateIndices = new ArrayList<Integer>(4);
			List<Integer> normalIndices = new ArrayList<Integer>(4);
			boolean first = true;
			while (true)
			{
				if (first)
				{
					first = false;
				}
				else
				{
					skipBackSlash(tokenizer);
				}
				if (tokenizer.nextToken() == StreamTokenizer.TT_EOL)
				{
					break;
				}
				else
				{
					tokenizer.pushBack();
				}
				// Read vertex index
				int vertexIndex = parseInteger(tokenizer) - 1;
				if (vertexIndex < 0)
				{
					vertexIndex += this.vertices.size() + 1;
				}
				vertexIndices.add(vertexIndex);
				
				if (tokenizer.nextToken() != '/')
				{
					// f v  
					tokenizer.pushBack();
				}
				else
				{
					if (tokenizer.nextToken() != '/')
					{
						// f v/vt : read texture coordinate index 
						tokenizer.pushBack();
						int textureCoordinateIndex = parseInteger(tokenizer) - 1;
						if (textureCoordinateIndex < 0)
						{
							textureCoordinateIndex += this.textureCoordinates.size() + 1;
						}
						textureCoordinateIndices.add(textureCoordinateIndex);
						tokenizer.nextToken();
					}
					if (tokenizer.ttype == '/')
					{
						//    f v//vn 
						// or f v/vt/vn : read normal index
						int normalIndex = parseInteger(tokenizer) - 1;
						if (normalIndex < 0)
						{
							normalIndex += this.normals.size() + 1;
						}
						normalIndices.add(normalIndex);
					}
					else
					{
						tokenizer.pushBack();
					}
				}
			}
			tokenizer.pushBack();
			tokenizer.wordChars('/', '/');
			if (textureCoordinateIndices.size() != 0 && textureCoordinateIndices.size() != vertexIndices.size())
			{
				// Ignore unconsistent texture coordinate 
				textureCoordinateIndices.clear();
			}
			if (normalIndices.size() != 0 && normalIndices.size() != vertexIndices.size())
			{
				// Ignore unconsistent normals 
				normalIndices.clear();
			}
			if (vertexIndices.size() > 2)
			{
				this.currentGroup.addGeometry(
						new Face(vertexIndices, textureCoordinateIndices, normalIndices, this.currentMaterial));
			}
		}
		else if ("g".equals(tokenizer.sval) || "o".equals(tokenizer.sval))
		{
			// Read group name g name 
			//  or object name o name
			boolean smoothingGroup = this.currentGroup.isSmooth();
			if (tokenizer.nextToken() == StreamTokenizer.TT_WORD)
			{
				this.currentGroup = this.groups.get(tokenizer.sval);
				if (this.currentGroup == null)
				{
					this.currentGroup = new Group(tokenizer.sval);
					this.groups.put(this.currentGroup.getName(), this.currentGroup);
				}
			}
			else if (tokenizer.ttype == StreamTokenizer.TT_EOL)
			{
				// Use default group
				this.currentGroup = this.groups.get("default");
				tokenizer.pushBack();
			}
			else
			{
				throw new IncorrectFormatException("Expected group or object name at line " + tokenizer.lineno());
			}
			this.currentGroup.setSmooth(smoothingGroup);
			// Skip other names
			while (tokenizer.nextToken() == StreamTokenizer.TT_WORD)
			{}
			tokenizer.pushBack();
		}
		else if ("s".equals(tokenizer.sval))
		{
			// Read smoothing group s n 
			//                   or s off
			if (tokenizer.nextToken() == StreamTokenizer.TT_WORD)
			{
				this.currentGroup.setSmooth(!"off".equals(tokenizer.sval));
			}
			else
			{
				throw new IncorrectFormatException("Expected smoothing group or off at line " + tokenizer.lineno());
			}
		}
		else if ("usemtl".equals(tokenizer.sval))
		{
			// Read the material name usemtl name (tolerating space in the name)
			tokenizer.wordChars(' ', ' ');
			int usemtlToken = tokenizer.nextToken();
			tokenizer.whitespaceChars(' ', ' ');
			if (usemtlToken == StreamTokenizer.TT_WORD)
			{
				this.currentMaterial = tokenizer.sval;
			}
			else
			{
				throw new IncorrectFormatException("Expected material name at line " + tokenizer.lineno());
			}
		}
		else if ("mtllib".equals(tokenizer.sval))
		{
			// Read characters following mtllib in case they contain a file name with spaces 
			tokenizer.wordChars(' ', ' ');
			int mtllibToken = tokenizer.nextToken();
			tokenizer.whitespaceChars(' ', ' ');
			if (mtllibToken == StreamTokenizer.TT_WORD)
			{
				String mtllibString = tokenizer.sval.trim();
				// First try to parse space separated library files
				int validLibCount = 0;
				String[] libs = mtllibString.split(" ");
				for (String lib : libs)
				{
					if (parseMaterial(lib, baseUrl))
					{
						validLibCount++;
					}
				}
				if (libs.length > 1 && validLibCount == 0)
				{
					// Even if not in format specifications, give a chance to file names with spaces
					parseMaterial(mtllibString, baseUrl);
				}
			}
			else
			{
				throw new IncorrectFormatException("Expected material library at line " + tokenizer.lineno());
			}
		}
		else
		{
			// Skip other lines (including comment lines starting by #)
			int token;
			do
			{
				token = tokenizer.nextToken();
			} while (token != StreamTokenizer.TT_EOL && token != StreamTokenizer.TT_EOF);
			tokenizer.pushBack();
		}
		
		int token = tokenizer.nextToken();
		if (token != StreamTokenizer.TT_EOL && token != StreamTokenizer.TT_EOF)
		{
			throw new IncorrectFormatException("Expected end of line at line " + tokenizer.lineno());
		}
	}
	
	/**
	 * Returns a new tokenizer for an OBJ or MTL stream.
	 */
	private static StreamTokenizer createTokenizer(Reader reader)
	{
		StreamTokenizer tokenizer = new StreamTokenizer(reader);
		tokenizer.resetSyntax();
		tokenizer.eolIsSignificant(true);
		
		// All printable ASCII characters
		tokenizer.wordChars('!', '~');
		// Let's tolerate other ISO-8859-1 characters
		tokenizer.wordChars(0x80, 0xFF);
		
		tokenizer.whitespaceChars(' ', ' ');
		tokenizer.whitespaceChars('\n', '\n');
		tokenizer.whitespaceChars('\r', '\r');
		tokenizer.whitespaceChars('\t', '\t');
		return tokenizer;
	}
	
	/**
	 * Returns the integer contained in the next token. 
	 */
	private static int parseInteger(StreamTokenizer tokenizer) throws IOException
	{
		if (tokenizer.nextToken() != StreamTokenizer.TT_WORD)
		{
			throw new IncorrectFormatException("Expected an integer at line " + tokenizer.lineno());
		}
		else
		{
			try
			{
				return Integer.parseInt(tokenizer.sval);
			}
			catch (NumberFormatException ex)
			{
				throw new IncorrectFormatException(
						"Found " + tokenizer.sval + " instead of an integer at line " + tokenizer.lineno());
			}
		}
	}
	
	/**
	 * Returns the number contained in the next token. 
	 */
	private static float parseNumber(StreamTokenizer tokenizer) throws IOException
	{
		if (tokenizer.nextToken() != StreamTokenizer.TT_WORD)
		{
			throw new IncorrectFormatException("Expected a number at line " + tokenizer.lineno());
		}
		else
		{
			try
			{
				return Float.parseFloat(tokenizer.sval);
			}
			catch (NumberFormatException ex)
			{
				throw new IncorrectFormatException(
						"Found " + tokenizer.sval + " instead of a number at line " + tokenizer.lineno());
			}
		}
	}
	
	/**
	 * Skips the back slash in the next token if it's followed by a new line.  
	 */
	private static void skipBackSlash(StreamTokenizer tokenizer) throws IOException
	{
		tokenizer.ordinaryChar('\\');
		if (tokenizer.nextToken() == '\\')
		{
			if (tokenizer.nextToken() != StreamTokenizer.TT_EOL)
			{
				throw new IncorrectFormatException("Expected new line after \\ character");
			}
		}
		else
		{
			tokenizer.pushBack();
		}
		tokenizer.wordChars('\\', '\\');
	}
	
	/**
	 * Parses appearances from the given material file and returns <code>true</code> if the file exists.
	 */
	private boolean parseMaterial(String file, URL baseUrl)
	{
		InputStream in = null;
		try
		{
			if (baseUrl != null)
			{
				in = openStream(new URL(baseUrl, file.replace("%", "%25").replace("#", "%23")), this.useCaches);
			}
			else
			{
				in = new FileInputStream(file);
			}
		}
		catch (IOException ex)
		{}
		
		if (in != null)
		{
			try
			{
				this.appearances.putAll(parseMaterialStream(new BufferedReader(new InputStreamReader(in, "ISO-8859-1")),
						baseUrl, this.useCaches));
				return true;
			}
			catch (IOException ex)
			{
				throw new ParsingErrorException(ex.getMessage());
			}
			finally
			{
				try
				{
					in.close();
				}
				catch (IOException ex)
				{
					throw new ParsingErrorException(ex.getMessage());
				}
			}
		}
		else
		{
			return false;
		}
	}
	
	/**
	 * Parses a map of appearances parsed from the given stream. 
	 */
	private static Map<String, Appearance> parseMaterialStream(Reader reader, URL baseUrl, Boolean useCaches)
			throws IOException
	{
		Map<String, Appearance> appearances = new HashMap<String, Appearance>();
		Appearance currentAppearance = null;
		StreamTokenizer tokenizer = createTokenizer(reader);
		while (tokenizer.nextToken() != StreamTokenizer.TT_EOF)
		{
			switch (tokenizer.ttype)
			{
				case StreamTokenizer.TT_WORD:
					currentAppearance = parseMaterialLine(tokenizer, appearances, currentAppearance, baseUrl,
							useCaches);
					break;
				case StreamTokenizer.TT_EOL:
					break;
				default:
					throw new IncorrectFormatException(
							"Unexpected token " + tokenizer.sval + " at row " + tokenizer.lineno());
			}
		}
		return appearances;
	}
	
	/**
	 * Parses the line starting with a word describing a material.
	 */
	private static Appearance parseMaterialLine(StreamTokenizer tokenizer, Map<String, Appearance> appearances,
			Appearance currentAppearance, URL baseUrl, Boolean useCaches) throws IOException
	{
		if ("newmtl".equals(tokenizer.sval))
		{
			// Read material name newmtl name (tolerating space in the name)
			tokenizer.wordChars(' ', ' ');
			int newmtlToken = tokenizer.nextToken();
			tokenizer.whitespaceChars(' ', ' ');
			if (newmtlToken == StreamTokenizer.TT_WORD)
			{
				currentAppearance = new Appearance();
				appearances.put(tokenizer.sval, currentAppearance);
				try
				{
					currentAppearance.setName(tokenizer.sval);
				}
				catch (NoSuchMethodError ex)
				{
					// Don't set name with Java 3D < 1.4
				}
			}
			else
			{
				throw new IncorrectFormatException("Expected material name at line " + tokenizer.lineno());
			}
		}
		else if ("Ka".equals(tokenizer.sval))
		{
			// Read ambient color Ka r g b
			Color3f ambientColor = new Color3f(parseNumber(tokenizer), parseNumber(tokenizer), parseNumber(tokenizer));
			if (currentAppearance != null)
			{
				Material material = getMaterial(currentAppearance);
				material.setAmbientColor(ambientColor);
			}
		}
		else if ("Kd".equals(tokenizer.sval))
		{
			// Read diffuse or emissive color Kd r g b
			Color3f diffuseColor = new Color3f(parseNumber(tokenizer), parseNumber(tokenizer), parseNumber(tokenizer));
			if (currentAppearance != null)
			{
				OBJMaterial material = getMaterial(currentAppearance);
				material.setDiffuseColor(diffuseColor);
				currentAppearance
						.setColoringAttributes(new ColoringAttributes(diffuseColor, ColoringAttributes.SHADE_GOURAUD));
			}
		}
		else if ("Ks".equals(tokenizer.sval))
		{
			// Read specular color Ks r g b
			Color3f specularColor = new Color3f(parseNumber(tokenizer), parseNumber(tokenizer), parseNumber(tokenizer));
			if (currentAppearance != null)
			{
				OBJMaterial material = getMaterial(currentAppearance);
				if (!material.isIlluminationModelSet() || material.getIlluminationModel() >= 2)
				{
					material.setSpecularColor(specularColor);
				}
				else
				{
					material.setSpecularColor(0, 0, 0);
				}
			}
		}
		else if ("Ns".equals(tokenizer.sval))
		{
			// Read shininess Ns val  with 0 <= val <= 1000
			float shininess = parseNumber(tokenizer);
			if (currentAppearance != null)
			{
				OBJMaterial material = getMaterial(currentAppearance);
				if (!material.isIlluminationModelSet() || material.getIlluminationModel() >= 2)
				{
					// Use shininess at a max value equal to 128
					material.setShininess(Math.max(1f, Math.min(shininess, 128f)));
				}
				else
				{
					material.setShininess(1f);
				}
			}
		}
		else if ("Ni".equals(tokenizer.sval))
		{
			// Read optical density Ni val  
			float opticalDensity = parseNumber(tokenizer);
			if (currentAppearance != null)
			{
				OBJMaterial material = getMaterial(currentAppearance);
				material.setOpticalDensity(opticalDensity);
			}
		}
		else if ("sharpness".equals(tokenizer.sval))
		{
			// Read sharpness sharpness val  
			float sharpness = parseNumber(tokenizer);
			if (currentAppearance != null)
			{
				OBJMaterial material = getMaterial(currentAppearance);
				material.setSharpness(sharpness);
			}
		}
		else if ("d".equals(tokenizer.sval))
		{
			// Read transparency d val  with 0 <= val <= 1
			if (tokenizer.nextToken() == StreamTokenizer.TT_WORD)
			{
				if ("-halo".equals(tokenizer.sval))
				{
					// Ignore halo transparency
					parseNumber(tokenizer);
				}
				else
				{
					tokenizer.pushBack();
					float transparency = parseNumber(tokenizer);
					if (currentAppearance != null)
					{
						if (transparency >= 1)
						{
							currentAppearance.setTransparencyAttributes(null);
						}
						else
						{
							currentAppearance.setTransparencyAttributes(new TransparencyAttributes(
									TransparencyAttributes.NICEST, 1f - Math.max(0f, transparency)));
						}
					}
				}
			}
			else
			{
				throw new IncorrectFormatException("Expected transparency factor at line " + tokenizer.lineno());
			}
		}
		else if ("illum".equals(tokenizer.sval))
		{
			// Read illumination setting illum n
			int illumination = parseInteger(tokenizer);
			if (currentAppearance != null)
			{
				OBJMaterial material = getMaterial(currentAppearance);
				material.setIlluminationModel(illumination);
				material.setLightingEnable(illumination >= 1);
				if (illumination <= 1)
				{
					material.setSpecularColor(0, 0, 0);
					material.setShininess(1f);
				}
			}
		}
		else if ("map_Kd".equals(tokenizer.sval))
		{
			// Read material texture map_Kd name
			// Search last parameter that matches image file name
			String imageFileName = null;
			while (tokenizer.nextToken() != StreamTokenizer.TT_EOL)
			{
				if (tokenizer.ttype == StreamTokenizer.TT_WORD)
				{
					imageFileName = tokenizer.sval;
				}
			}
			
			if (imageFileName != null)
			{
				InputStream in = null;
				try
				{
					URL textureImageUrl = baseUrl != null
							? new URL(baseUrl, imageFileName.replace("%", "%25").replace("#", "%23"))
							: new File(imageFileName).toURI().toURL();
					// Check texture image wasn't already loaded
					Texture texture = null;
					for (Appearance appearance : appearances.values())
					{
						Texture appearanceTexture = appearance.getTexture();
						if (appearanceTexture != null && textureImageUrl.equals(appearanceTexture.getUserData()))
						{
							currentAppearance.setTexture(appearanceTexture);
							texture = appearanceTexture;
						}
					}
					if (texture == null)
					{
						in = openStream(textureImageUrl, useCaches);
						BufferedImage textureImage = null;
						try
						{
							textureImage = ImageIO.read(in);
						}
						catch (ConcurrentModificationException ex)
						{
							// Try to read the image once more, 
							// see unfixed Java bug http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=6986863
							in.close();
							in = openStream(textureImageUrl, useCaches);
							textureImage = ImageIO.read(in);
						}
						
						if (textureImage != null)
						{
							TextureLoader textureLoader = new TextureLoader(textureImage);
							texture = textureLoader.getTexture();
							// Keep in user data the URL of the texture image
							texture.setUserData(textureImageUrl);
							currentAppearance.setTexture(texture);
						}
					}
				}
				catch (IOException ex)
				{
					// Ignore images at other format
				}
				catch (RuntimeException ex)
				{
					// Take into account exceptions of Java 3D 1.5 ImageException class
					// in such a way program can run in Java 3D 1.3.1
					if (ex.getClass().getName().equals("com.sun.j3d.utils.image.ImageException"))
					{
						// Ignore images not supported by TextureLoader
					}
					else
					{
						throw ex;
					}
				}
				finally
				{
					if (in != null)
					{
						in.close();
					}
				}
			}
			else
			{
				throw new IncorrectFormatException("Expected image file name at line " + tokenizer.lineno());
			}
			tokenizer.pushBack();
		}
		else
		{
			int token;
			do
			{
				token = tokenizer.nextToken();
			} while (token != StreamTokenizer.TT_EOL && token != StreamTokenizer.TT_EOF);
			tokenizer.pushBack();
		}
		
		int token = tokenizer.nextToken();
		if (token != StreamTokenizer.TT_EOL && token != StreamTokenizer.TT_EOF)
		{
			throw new IncorrectFormatException("Expected end of line at line " + tokenizer.lineno());
		}
		
		return currentAppearance;
	}
	
	/**
	 * Returns the material stored in the given <code>appearance</code>.
	 */
	private static OBJMaterial getMaterial(Appearance appearance)
	{
		OBJMaterial material = (OBJMaterial) appearance.getMaterial();
		if (material == null)
		{
			material = new OBJMaterial();
			appearance.setMaterial(material);
		}
		return material;
	}
	
	/**
	 * The coordinates indices of a geometry. 
	 */
	private static abstract class Geometry
	{
		private int[] vertexIndices;
		private int[] textureCoordinateIndices;
		private String material;
		
		public Geometry(List<Integer> vertexIndices, List<Integer> textureCoordinateIndices, String material)
		{
			this.vertexIndices = new int[vertexIndices.size()];
			for (int i = 0; i < this.vertexIndices.length; i++)
			{
				this.vertexIndices[i] = vertexIndices.get(i);
			}
			if (textureCoordinateIndices.size() != 0)
			{
				this.textureCoordinateIndices = new int[textureCoordinateIndices.size()];
				for (int i = 0; i < this.textureCoordinateIndices.length; i++)
				{
					this.textureCoordinateIndices[i] = textureCoordinateIndices.get(i);
				}
			}
			this.material = material;
		}
		
		public int[] getVertexIndices()
		{
			return this.vertexIndices;
		}
		
		public int[] getTextureCoordinateIndices()
		{
			return this.textureCoordinateIndices;
		}
		
		public boolean hasTextureCoordinateIndices()
		{
			return this.textureCoordinateIndices != null && this.textureCoordinateIndices.length > 0;
		}
		
		public String getMaterial()
		{
			return this.material;
		}
	}
	
	/**
	 * The coordinates indices of a line. 
	 */
	private static class Line extends Geometry
	{
		public Line(List<Integer> vertexIndices, List<Integer> textureCoordinateIndices, String material)
		{
			super(vertexIndices, textureCoordinateIndices, material);
		}
	}
	
	/**
	 * The coordinates indices of a face. 
	 */
	private static class Face extends Geometry
	{
		private int[] normalIndices;
		
		public Face(List<Integer> vertexIndices, List<Integer> textureCoordinateIndices, List<Integer> normalIndices,
				String material)
		{
			super(vertexIndices, textureCoordinateIndices, material);
			if (normalIndices.size() != 0)
			{
				this.normalIndices = new int[normalIndices.size()];
				for (int i = 0; i < this.normalIndices.length; i++)
				{
					this.normalIndices[i] = normalIndices.get(i);
				}
			}
		}
		
		public int[] getNormalIndices()
		{
			return this.normalIndices;
		}
		
		public boolean hasNormalIndices()
		{
			return this.normalIndices != null && this.normalIndices.length > 0;
		}
	}
	
	/**
	 * A named group of geometries. 
	 */
	private static class Group
	{
		private final String name;
		private boolean smooth;
		private List<Geometry> geometries;
		
		public Group(String name)
		{
			this.name = name;
			this.geometries = new ArrayList<Geometry>();
		}
		
		public String getName()
		{
			return this.name;
		}
		
		public void setSmooth(boolean smooth)
		{
			this.smooth = smooth;
		}
		
		public boolean isSmooth()
		{
			return this.smooth;
		}
		
		public void addGeometry(Geometry face)
		{
			this.geometries.add(face);
		}
		
		public List<Geometry> getGeometries()
		{
			return this.geometries;
		}
	}
}
